package webproxy

import (
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"thais/cookie"
	"thais/database/redis"
	"thais/utils"
	"thais/utils/jwt"
	wl "thais/whitelist"
	"time"

	"github.com/PuerkitoBio/goquery"
	"github.com/bugfan/gosen"
	"github.com/bugfan/to"
)

const (
	TOKEN_SHARP    = `#`
	TOKEN_SOLIDI   = `/`
	TOKEN_DBSOLIDI = `//`
	TOKEN_HTTP     = `http`
	TOKEN_HTTPS    = `https`
	TOKEN_DOT      = `.`
	DENY_STRING    = `********`
)

var (
	Debug bool
)

type WebProxy interface {
	InjectJs(bool)
	Run(Config) error
}

func NewWebProxy() WebProxy {
	c := &Config{}
	return &myHandler{conf: c}
}

type myHandler struct {
	tagConfig      []*tag //goquery filter config
	conf           *Config
	injectJs       bool
	dstRequestHost string
}

func (s *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	defer func() { recover() }()
	urlStr := strings.Split(r.RequestURI, s.conf.Middle)
	utils.Zlog(utils.LogLevelInfo, "----> New request", "", fmt.Sprintf("url: %s", r.Proto+" "+r.Host+r.RequestURI))

	if len(urlStr) < 2 {
		w.WriteHeader(http.StatusNotFound)
	} else {
		scheme := `http://`
		if r.Proto == `HTTP/2.0` { // 判断下原始的协议
			scheme = `https://`
		}

		h := strings.Split(r.Host, ":")
		s.conf.Addr = h[0]
		s.conf.OtherAddr = h[0]

		list, err := wl.GetList(r.Header.Get(cookie.COOKIE_AUTH_KEY))
		if err != nil {
			utils.Zlog(utils.LogLevelInfo, "Get WhiteList Error:", "", to.String(err))
			return
		}
		url := `http://` + urlStr[1]
		if !wl.Match(list, url) {
			utils.Zlog(utils.LogLevelInfo, "Not Match:", "", url)
			w.WriteHeader(http.StatusForbidden)
			return
		}

		s.execReq(w, r, urlStr[1], scheme, urlStr[0], r.Host)
	}
	return
}

var parseURL = url.Parse

// 处理响应头
func modiResqHeader(header *http.Header) {
	h := []string{"Content-Security-Policy"}
	for _, v := range h {
		header.Del(v)
	}
}

// referer must be a result
func (s *myHandler) getOldReferer(str string) string {
	strs := strings.Split(str, s.conf.Middle)
	if len(strs) < 2 {
		return str
	}
	u, err := url.Parse(strs[0])
	if err != nil {
		return str
	}
	sch := u.Scheme
	if sch == "http" || sch == "" {
		return `http://` + strs[1]
	} else if sch == "https" {
		return `https://` + strs[1]
	}
	return strs[1]
}
func getHost(str string) string {
	u, err := parseURL(str)
	if err != nil {
		return str
	}
	h := u.Host
	return h
}

func (s myHandler) newRequest(method, url string, body io.Reader, myheader http.Header) (*http.Request, error) {
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}
	u, err := parseURL(url) // Just url.Parse (url is shadowed for godoc).
	if err != nil {
		return nil, err
	}
	header := make(http.Header)
	header = cloneHeader(myheader)
	h := []string{"Accept-Encoding"}
	for _, v := range h {
		header.Del(v)
	}
	referer := header.Get("Referer")
	if referer != "" {
		header.Set("Referer", s.getOldReferer(referer))
	}
	header.Set("Origin", s.conf.Scheme+`://`+getHost(url))
	header.Set("Host", getHost(url))
	req.Header = header
	req.URL = u
	req.Host = u.Host
	return req, err
}
func (s *myHandler) execReq(w http.ResponseWriter, r *http.Request, uri, scheme, uriPrefix, reqHost string) {
	var (
		res    *http.Response
		req    *http.Request
		err    error
		newURL string
	)
	newURL = fmt.Sprintf(scheme+"%s", uri)
	req, err = s.newRequest(r.Method, newURL, r.Body, r.Header)

	s.dstRequestHost = req.Host

	pcookie, cookieErr := r.Cookie(cookie.COOKIE_NAME) // 取出我需要的thais cookie
	req.Header.Del("Cookie")                           // 删除无用cookie

	myCookieName := "" // 存放实际的uuid
	throwCookie := true

	// 如果缓存中有此cookie的cookiejar json数据，就读取并转为cookiejar对象
	cookieJarStr := ""

	if cookieErr == nil && throwCookie {

		// 从jwt解出uuid ,可能会解不开，也可能会过期,异常我就跳出去
		jwtData, err := jwt.VerifyJWT3(pcookie.Value)
		if err != nil {
			throwCookie = false
			goto THROW_COOKIE_BREAK
		}
		myCookieName = to.String(jwtData)

		cookieJarStr = redis.Proxy.Get(myCookieName) //jars.Data[myCookieName]
		cookieJar := cookie.Jar{}
		err = json.Unmarshal([]byte(cookieJarStr), &cookieJar)
		if cookieJarStr != "" {
			// 可以查询到，说明此键值存在，需要让它活跃
			redis.Proxy.Expire(myCookieName, cookie.COOKIE_NAME_TTL)
			for _, v := range cookieJar.Cookies(req.URL) {
				req.AddCookie(v)
			}
		}
	}
THROW_COOKIE_BREAK:

	// 为每一个请求new一个cookiejar和client
	jar := cookie.NewCookieJar()
	c := NewHttpClient(req.URL.String())
	c.Jar = jar

	res, err = c.Do(req)
	if err != nil {
		utils.Zlog(utils.LogLevelError, "Client Do Error", to.String(err), "")
		w.WriteHeader(http.StatusBadGateway)
		return
	}

	defer res.Body.Close()
	res.Header.Del("Set-Cookie")         // 把写到浏览器的cookie清除
	httpCookies := jar.Cookies(req.URL)  // 读取cookie
	if cookieErr == nil && throwCookie { // 如果浏览器已经有pcookie ，就根据浏览器cookie的value设置cookiejar到缓存
		jar := &cookie.Jar{}
		if cookieJarStr == "" {
			jar = cookie.NewCookieJar()
		} else {
			err = json.Unmarshal([]byte(cookieJarStr), &jar)
			if err != nil {
				utils.Zlog(utils.LogLevelError, "JarStr to CookieJar Unmarshal Error", to.String(err), "")
			}
		}
		jar.SetCookies(req.URL, httpCookies)
		jarBs, _ := json.Marshal(jar)
		redis.Proxy.Set(myCookieName, to.String(jarBs), cookie.COOKIE_NAME_TTL)
	} else {
		http.SetCookie(w, cookie.NewCookie())
	}

	// 开始处理 header & body
	modiResqHeader(&res.Header)
	copyHeader(w.Header(), res.Header)
	resConType := res.Header.Get("Content-Type")
	utils.Zlog(utils.LogLevelInfo, "<---- Response ", "", fmt.Sprintf("StatusCode:%v Content-Type:%s", res.StatusCode, resConType))
	if len(resConType) > 6 {
		if resConType[0:4] == `text` { // document attr replace
			byteStream, _ := ioutil.ReadAll(res.Body)
			//	如果是html 插js，并修改长度
			if resConType[0:7] == `text/ht` && s.injectJs {
				oldLen := len(byteStream)
				_, newHtml := s.inject(string(byteStream), `<head>`, oldLen, reqHost, uriPrefix)

				doc, _ := goquery.NewDocumentFromReader(strings.NewReader(newHtml))
				for _, v := range s.tagConfig {
					doc.Find(v.Find).Each(func(i int, sel *goquery.Selection) {
						val, e := sel.Attr(v.Attr)
						if !e {
							return
						}
						if v.Find == "head link" || v.Find == "body script" {
							sel.SetAttr("crossorigin", "")
							sel.SetAttr("integrity", "")
						}
						sel.SetAttr(v.Attr, s.dealUrl(r, val, uri, uriPrefix, reqHost))
					})
				}

				newHtml, _ = goquery.OuterHtml(doc.Selection)

				// ioutil.WriteFile("./out.html", []byte(outHtml), 0666)
				w.Header().Set("Content-Length", to.String(len(newHtml)))
				w.Write([]byte(newHtml))
				return
			}
			w.Write(byteStream)
		} else if resConType == `application/javascript` { // all text regular replace

			byteStream, _ := ioutil.ReadAll(res.Body)
			w.Write(byteStream)
		} else {
			io.Copy(w, res.Body)
			return
		}
	}
}

// 插入js代码
func (s *myHandler) inject(str string, position string, n int, reqHost string, uriPrefix string) (int, string) {
	finalJs := ""
	strs := strings.Split(reqHost, ":")
	// '127.0.0.1:8080/test/xxxx/', '127.0.0.1:9090/test/xxxx/'
	varHttp, varHttps, proxyHost := "", "", ""
	if len(strs) >= 1 {
		varHttp = strs[0] + ":" + s.conf.Port + uriPrefix + s.conf.Middle
		varHttps = strs[0] + ":" + s.conf.OtherPort + uriPrefix + s.conf.Middle
		proxyHost = strs[0]
	} else {
		utils.Zlog(utils.LogLevelError, "Substitute variable error", "", reqHost)
		finalJs = s.conf.Script
	}
	finalJs = fmt.Sprintf(s.conf.Script, varHttp, varHttps, s.dstRequestHost, proxyHost,
		`http://`+strs[0]+`:`+s.conf.Port+s.conf.Middle+`thais/static/thais.js`,
		`http://`+strs[0]+`:`+s.conf.Port+s.conf.Middle+`thais/jsonp?callback=genWhiteList`)

	se := gosen.New(finalJs)
	se.Tokens = []string{position}
	newStr, err := se.Filter(str)
	if err != nil {
		utils.Zlog(utils.LogLevelError, "bugfan gosen Filter error", to.String(err), "")
		return n, str
	}
	return len(newStr), newStr
}

func (s *myHandler) dealUrl(r *http.Request, val, def, uriPrefix, reqHost string) string {
	utils.Zlog(utils.LogLevelInfo, "--> In Link", "", val)
	list, err := wl.GetList(r.Header.Get(cookie.COOKIE_AUTH_KEY))
	if err != nil {
		utils.Zlog(utils.LogLevelInfo, "<-- Out Link", to.String(err), DENY_STRING)
		return DENY_STRING
	}
	nativeUrl := s.deal(val, def, uriPrefix, reqHost)
	urls := strings.Split(nativeUrl, s.conf.Middle)
	if len(urls) < 2 {
		utils.Zlog(utils.LogLevelInfo, "<-- Out Link", "", urls[0])
		return urls[0]
	} else {
		if wl.Match(list, `http://`+urls[1]) {
			utils.Zlog(utils.LogLevelInfo, "<-- Out Link", "", nativeUrl)
			return nativeUrl
		} else {
			utils.Zlog(utils.LogLevelInfo, "<-- Out Link", "Not Match", DENY_STRING)
			return DENY_STRING
		}
	}
}

/*
*	val:dom元素的原始链接 def:地址栏的请求地址[/xxxx/]之后的部分 uriPrefix:地址栏的请求地址[/xxxx/]之前的部分
 */
func (s *myHandler) deal(val, def, uriPrefix, reqHost string) string {
	// /
	if val == TOKEN_SOLIDI {
		return TOKEN_SOLIDI + reqHost + s.conf.Middle + def + val

	}
	// #
	if val == TOKEN_SHARP {
		return reqHost + s.conf.Middle + def
	}
	// 0.防崩
	defer func() { recover() }()
	// 1.检查长度
	if len(val) < 4 {
		if len(val) >= 1 && val[0:1] == `/` {
			str := TOKEN_DBSOLIDI + reqHost + uriPrefix + s.conf.Middle + def + val
			return str
		}
		return val
	}
	// 2.1 ./images/step3.png
	if val[0:1] == TOKEN_DOT && val[1:2] == TOKEN_SOLIDI && val[2:3] != TOKEN_SOLIDI {
		return val
	}
	// 2.2 data:xxxx
	if val[0:5] == `data:` {
		return val
	}
	// 2.x 静态目录
	if strings.Contains(val, strings.Split(reqHost, ":")[0]) && strings.Contains(val, s.conf.Middle) {
		return val
	}
	// 2.3 image/abc.jpg
	if val[0:1] != TOKEN_SOLIDI && val[0:4] != TOKEN_HTTP && val[0:2] != TOKEN_DBSOLIDI {
		if val[0:7] == "javascr" {
			return val
		}
		str := TOKEN_DBSOLIDI + reqHost + uriPrefix + s.conf.Middle + def + TOKEN_SOLIDI + val
		return str
	}
	// 3.检查头
	if val[0:1] != TOKEN_SOLIDI && val[0:4] != TOKEN_HTTP {
		return val
	}
	var (
		urlObj *url.URL = new(url.URL)
		err    error
	)
	// 4.0 替换
	urlObj, err = url.Parse(val)
	if err != nil {
		return val
	}
	if val[0:1] == TOKEN_SOLIDI && val[1:2] != TOKEN_SOLIDI { // /join?name=xxx
		return TOKEN_DBSOLIDI + reqHost + uriPrefix + s.conf.Middle + getDstHost(def) + val
	} else {
		var str string
		tmpHost := urlObj.Host
		if val[0:5] == TOKEN_HTTPS {
			urlObj.Host = s.conf.Addr + ":" + s.conf.OtherPort
			str = urlObj.String()
			oldHost := s.conf.OtherAddr + ":" + s.conf.OtherPort
			str = strings.Replace(str, oldHost, oldHost+uriPrefix+s.conf.Middle+tmpHost, -1) // 防止替换成utf8字符
		} else {
			urlObj.Host = reqHost
			str = urlObj.String()
			oldHost := reqHost
			httpHost := s.conf.Addr + ":" + s.conf.Port
			str = strings.Replace(str, oldHost, oldHost+uriPrefix+s.conf.Middle+tmpHost, -1) // 防止替换成utf8字符
			if val[0:4] == TOKEN_HTTP {
				str = strings.Replace(str, oldHost, httpHost, -1) // 防止替换成utf8字符
			}
		}
		// 4.1 处理带redirect
		bi := findIndex(str, "http")
		if bi > 10 {
			ei := bi
			if str[bi+5:bi+8] == `://` {
				ei = bi + 9
			} else if str[bi+4:bi+7] == `://` {
				ei = ei + 8
			}
			str = str[0:ei-1] + reqHost + uriPrefix + s.conf.Middle + str[ei-1:]
		}
		return str
	}
}

func getDstHost(dst string) string {
	ind := strings.Index(dst, `/`)
	if ind < 0 {
		return dst
	}
	return dst[0:ind]
}

// 4.1 找redirect方式的第二个链接的替换部分
func findIndex(str, find string) (index int) {
	strs := strings.Split(str, "?")
	if len(strs) < 2 {
		return -1
	}
	tmpInd := strings.Index(str, "?")
	paths := strings.Split(strs[1], "=")
	findLen := len(find)
	for k, path := range paths {
		if k == 0 {
			continue
		}
		if len(path) > findLen && path[0:findLen] == find {
			return strings.Index(strs[1], find) + 1 + tmpInd
		}
	}
	return -1
}

func copyHeader(dst, src http.Header) {
	for k, vv := range src {
		for _, v := range vv {
			dst.Add(k, v)
		}
	}
}

func cloneHeader(h http.Header) http.Header {
	h2 := make(http.Header, len(h))
	for k, vv := range h {
		vv2 := make([]string, len(vv))
		copy(vv2, vv)
		h2[k] = vv2
	}
	return h2
}

func (s *myHandler) loadConfig(c *Config) {
	s.conf = c
	err := json.Unmarshal([]byte(TAG_CONFIG), &s.tagConfig)
	if err != nil {
		utils.Zlog(utils.LogLevelFatal, "function:loadConfig -> Tag Config load error", to.String(err), "")
	}
}

func (s *myHandler) InjectJs(v bool) {
	s.injectJs = v
}

func (s *myHandler) Run(c Config) error {
	// 加载配置信息
	s.loadConfig(&c)

	mux := http.NewServeMux()
	mux.Handle("/", middleHandler(s))

	// 静态目录
	fileServer := http.FileServer(http.Dir("./static"))
	mux.Handle(s.conf.Middle+"thais/static/", http.StripPrefix(c.Middle+"thais/static/", middleHandler(fileServer)))

	if c.Scheme == "http" || c.Scheme == "" {
		// API 放到http server上面
		mux.Handle(s.conf.Middle+"thais/jsonp", middleHandler(http.HandlerFunc(callback)))
		mux.Handle(s.conf.Middle+"thais/login", middleHandler(http.HandlerFunc(login)))
		mux.Handle(s.conf.Middle+"thais/list", middleHandler(http.HandlerFunc(list)))
		if Debug {
			mux.HandleFunc(s.conf.Middle+"thais/testLogin", testLogin)
		}
		log.Printf("Server start up! [%s] [%s]\n", c.Scheme, c.Port)
		return (http.ListenAndServe(c.Addr+":"+c.Port, mux))
	} else if c.Scheme == "https" {
		log.Printf("Server start up! [%s] [%s]\n", c.Scheme, c.OtherPort)
		err := http.ListenAndServeTLS(c.Addr+":"+c.OtherPort, c.CertPath, c.KeyPath, mux)
		if err != nil {
			log.Fatal("Https Server run failed:", err)
		}
		return err
	} else {
		return errors.New(fmt.Sprintf("This protocol %s is not supported!", c.Scheme))
	}
}

// new a http client ,support ENV appoint proxy addr
func NewHttpClient(reqUrl string) *http.Client {
	netTransport := &http.Transport{
		Dial: func(netw, addr string) (net.Conn, error) {
			c, err := net.DialTimeout(netw, addr, time.Second*time.Duration(20))
			if err != nil {
				return nil, err
			}
			return c, nil
		},
		MaxIdleConnsPerHost:   20,                              //每个host最大空闲连接
		ResponseHeaderTimeout: time.Second * time.Duration(20), //数据收发5秒超时
		TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
	}
	proxyEnvName := "HTTP_PROXY_URL"
	if len(reqUrl) > 4 && reqUrl[0:5] == "https" {
		proxyEnvName = "HTTPS_PROXY_URL"
	}

	proxyUrl := os.Getenv(proxyEnvName)

	if proxyUrl != "" {
		proxy, err := url.Parse(proxyUrl)
		if err != nil {
			utils.Zlog(utils.LogLevelDebug, "NewHttpClient Error:", to.String(err), "")
			return nil
		}
		netTransport.Proxy = http.ProxyURL(proxy)
	}

	return &http.Client{
		Timeout:   time.Second * 20,
		Transport: netTransport,
	}
}
